﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MattDotson.GlobalToolkit
{
    /// <summary>
    /// An implementation of a currency value and a currency type.  The type provides overrides for simple arithmatic 
    /// operations.
    /// </summary>
    public struct Currency
    {
        /// <summary>
        /// Gets the empty (null) currency.
        /// </summary>
        /// <value>The empty.</value>
        public static Currency Empty { get { return new Currency(); } }

        /// <summary>
        /// Gets or sets the value.
        /// </summary>
        /// <value>The value.</value>
        public decimal Value { get; private set; }

        /// <summary>
        /// Gets or sets the currency info.
        /// </summary>
        /// <value>The currency info.</value>
        public CurrencyInfo CurrencyInfo { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="Currency"/> struct.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="currency">The currency.</param>
        public Currency(decimal value, CurrencyInfo currency)
            : this()
        {
            Guard.NotNull("currency", currency);

            this.Value = value;
            this.CurrencyInfo = currency;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Currency"/> struct.
        /// </summary>
        /// <param name="value">The value.</param>
        /// <param name="isoCode">The iso code.</param>
        public Currency(decimal value, string isoCode)
            : this()
        {
            Guard.NotNullOrEmpty("isoCode", isoCode);

            this.CurrencyInfo = CurrencyInfo.GetCurrencyInfo(isoCode);
            this.Value = value;

            if (null == this.CurrencyInfo)
            {
                //throw if we can't find the requested currency
                throw new CurrencyNotFoundException(isoCode);
            }
        }

        /// <summary>
        /// Compares the two currencies for equality
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns></returns>
        public static bool Equals(Currency left, Currency right)
        {
            if (Object.ReferenceEquals(left, right))
            {
                return true;
            }

            return left.CurrencyInfo == right.CurrencyInfo
                && left.Value == right.Value;
        }

        /// <summary>
        /// Indicates whether this instance and a specified object are equal.
        /// </summary>
        /// <param name="obj">Another object to compare to.</param>
        /// <returns>
        /// true if <paramref name="obj"/> and this instance are the same type and represent the same value; otherwise, false.
        /// </returns>
        public override bool Equals(object obj)
        {
            if (obj is Currency)
            {
                return Currency.Equals(this, (Currency)obj);
            }

            return false;
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(Currency left, Currency right)
        {
            return Currency.Equals(left, right);
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(Currency left, Currency right)
        {
            return !Currency.Equals(left, right);
        }

        /// <summary>
        /// Returns the hash code for this instance.
        /// </summary>
        /// <returns>
        /// A 32-bit signed integer that is the hash code for this instance.
        /// </returns>
        public override int GetHashCode()
        {
            if (null == this.CurrencyInfo)
            {
                return 0;
            }

            return this.CurrencyInfo.IsoCurrencyCode.GetHashCode() ^ this.Value.GetHashCode();
        }

        #region Arithmatic Operators

        /// <summary>
        /// Adds the specified left.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns></returns>
        public static Currency Add(Currency left, Currency right)
        {
            if (Currency.Empty == left)
            {
                return right;
            }
            else if (Currency.Empty == right)
            {
                return left;
            }
            else if (left.CurrencyInfo == right.CurrencyInfo)
            {
                return new Currency(left.Value + right.Value, left.CurrencyInfo);
            }
            else if (GlobalToolkitContext.CurrencyOptions.EnableImplicitConversion)
            {
                ICurrencyConverter converter = GlobalToolkitContext.ServiceLocator.Resolve<ICurrencyConverter>();
                if (null == converter)
                {
                    //Implicit conversion is enabled, but no currency converter is configured.
                    //todo: message from resources
                    throw new CurrencyException();
                }

                Currency converted = converter.Convert(right, left.CurrencyInfo);
                return Add(left, converted);
            }
            else
            {
                //currencies do not match and we can't do an implicit conversion
                throw new CurrencyException();
            }
        }

        /// <summary>
        /// Implements the operator +.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator +(Currency left, Currency right)
        {
            return Currency.Add(left, right);
        }


        /// <summary>
        /// Subtracts the specified left.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns></returns>
        public static Currency Subtract(Currency left, Currency right)
        {
            if (Currency.Empty == left)
            {
                return -right;
            }
            else if (Currency.Empty == right)
            {
                return left;
            }
            else if (left.CurrencyInfo == right.CurrencyInfo)
            {
                return new Currency(left.Value - right.Value, left.CurrencyInfo);
            }
            else
            {
                //need to convert
                ICurrencyConverter converter = null; //todo: use service locator

                if (null != converter && converter.CanConvert(right.CurrencyInfo, left.CurrencyInfo))
                {
                    Currency converted = converter.Convert(right, left.CurrencyInfo);
                    return Subtract(left, converted);
                }

                //they don't match and we can't convert
                throw new CurrencyException();
            }
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator -(Currency left, Currency right)
        {
            return Currency.Subtract(left, right);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, decimal multiplier)
        {
            if (Currency.Empty == currency)
            {
                return Currency.Empty;
            }

            return new Currency(currency.Value * multiplier, currency.CurrencyInfo);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, double multiplier)
        {
            return Multiply(currency, (decimal)multiplier);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, float multiplier)
        {
            return Multiply(currency, (decimal)multiplier);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, long multiplier)
        {
            return Multiply(currency, (decimal)multiplier);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, int multiplier)
        {
            return Multiply(currency, (decimal)multiplier);
        }

        /// <summary>
        /// Multiplies the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="multiplier">The multiplier.</param>
        /// <returns></returns>
        public static Currency Multiply(Currency currency, short multiplier)
        {
            return Multiply(currency, (decimal)multiplier);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, decimal right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, float right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, double right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, long right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, int right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(Currency left, short right)
        {
            return Currency.Multiply(left, right);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(decimal left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(float left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(double left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(long left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(int left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Implements the operator *.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator *(short left, Currency right)
        {
            return Currency.Multiply(right, left);
        }

        /// <summary>
        /// Divides the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns></returns>
        public static Currency Divide(Currency currency, decimal divisor)
        {
            if (Currency.Empty == currency)
            {
                return Currency.Empty;
            }

            return new Currency(currency.Value / divisor, currency.CurrencyInfo);
        }

        /// <summary>
        /// Divides the specified currency by the divisor.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns>The quotient</returns>
        public static Currency Divide(Currency currency, double divisor)
        {
            return Currency.Divide(currency, (decimal)divisor);
        }

        /// <summary>
        /// Divides the specified currency by the divisor.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns>The quotient</returns>
        public static Currency Divide(Currency currency, float divisor)
        {
            return Currency.Divide(currency, (decimal)divisor);
        }

        /// <summary>
        /// Divides the specified currency by the divisor.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns>The quotient</returns>
        public static Currency Divide(Currency currency, long divisor)
        {
            return Currency.Divide(currency, (decimal)divisor);
        }

        /// <summary>
        /// Divides the specified currency by the divisor.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns>The quotient</returns>
        public static Currency Divide(Currency currency, int divisor)
        {
            return Currency.Divide(currency, (decimal)divisor);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, decimal right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, double right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, float right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, long right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, int right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Implements the operator /.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator /(Currency left, short right)
        {
            return Currency.Divide(left, right);
        }

        /// <summary>
        /// Divides the specified currency by the divisor.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <param name="divisor">The divisor.</param>
        /// <returns>The quotient</returns>
        public static Currency Divide(Currency currency, short divisor)
        {
            return Currency.Divide(currency, (decimal)divisor);
        }

        /// <summary>
        /// Negates the specified currency.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <returns></returns>
        public static Currency Negate(Currency currency)
        {
            if (Currency.Empty == currency)
            {
                return Currency.Empty;
            }

            return new Currency(-currency.Value, currency.CurrencyInfo);
        }

        /// <summary>
        /// Implements the operator -.
        /// </summary>
        /// <param name="currency">The currency.</param>
        /// <returns>The result of the operator.</returns>
        public static Currency operator -(Currency currency)
        {
            return Currency.Negate(currency);
        }

        #endregion

        #region Currency Conversion

        /// <summary>
        /// Converts one currency to another using the default currency converter
        /// </summary>
        /// <param name="from">The <see cref="Currency"/> value to convert from.</param>
        /// <param name="toCurrency">The currency to convert to</param>
        /// <returns></returns>
        public static Currency ConvertTo(Currency from, CurrencyInfo toCurrency)
        {
            var converter = GlobalToolkitContext.ServiceLocator.Resolve<ICurrencyConverter>();
            if (null == converter)
            {
                //default converter is not configured
                //TODO: add exception resource message
                throw new CurrencyConversionException();
            }

            return Currency.ConvertTo(from, toCurrency, converter);
        }

        /// <summary>
        /// Converts one currency to another using the specified currency converter
        /// </summary>
        /// <param name="from">The <see cref="Currency"/> value to convert from.</param>
        /// <param name="toCurrency">The currency to convert to</param>
        /// <param name="converter">The converter.</param>
        /// <returns></returns>
        public static Currency ConvertTo(Currency from, CurrencyInfo toCurrency, ICurrencyConverter converter)
        {
            Guard.NotNull("toCurrency", toCurrency);
            Guard.NotNull("converter", converter);

            return converter.Convert(from, toCurrency);
        }

        /// <summary>
        /// Converts a currency value to the currency specified by <paramref name="toCurrencyCode"/>
        /// </summary>
        /// <param name="from">From.</param>
        /// <param name="toCurrencyCode">To currency code.</param>
        /// <returns></returns>
        public static Currency ConvertTo(Currency from, string toCurrencyCode)
        {
            CurrencyInfo toCurrency = CurrencyInfo.GetCurrencyInfo(toCurrencyCode);

            return Currency.ConvertTo(from, toCurrency);
        }

        /// <summary>
        /// Converts the current <see cref="Currency"/> object to the specified 
        /// <see cref="CurrencyInfo"/> in <paramref name="toCurrency"/>.
        /// </summary>
        /// <param name="toCurrency">To currency.</param>
        /// <returns>If this is <see cref="Currency.Empty"/> then <see cref="Currency.Empty"/>; otherwise a converted 
        /// <see cref="Currency"/> value in the currency specified by <paramref name="toCurrency"/>.
        /// </returns>
        public Currency ConvertTo(CurrencyInfo toCurrency)
        {
            return Currency.ConvertTo(this, toCurrency);
        }

        /// <summary>
        /// Converts the current <see cref="Currency"/> object to the specified
        /// <see cref="CurrencyInfo"/> in <paramref name="toCurrency"/>.
        /// </summary>
        /// <param name="toCurrency">To currency.</param>
        /// <param name="converter">The converter.</param>
        /// <returns>
        /// If this is <see cref="Currency.Empty"/> then <see cref="Currency.Empty"/>; otherwise a converted
        /// <see cref="Currency"/> value in the currency specified by <paramref name="toCurrency"/>.
        /// </returns>
        public Currency ConvertTo(CurrencyInfo toCurrency, ICurrencyConverter converter)
        {
            return Currency.ConvertTo(this, toCurrency, converter);
        }

        #endregion
    }
}